<?php
// @formatter:off
/**
 * @file WCPageCache.php
 * @author Alejandro Dario Simi
 * @date $Date: 2013-08-20 00:23:06 +0000 (Tue, 20 Aug 2013) $
 *
 * $Id: WCPageCache.php 102 2013-08-20 00:23:06Z daemonraco@gmail.com $
 * $URL: http://wcomix.googlecode.com/svn/tags/wcomix-1.0.0.1/includes/WCPageCache.php $
 */
// @formatter:on

define("WC_CACHE_COMPRESS_LEVEL", 2);

/**
 * @class WCPageCache This class is the singleton manager for cached information
 * like pages, search results and others. It holds the logic to store and maintain
 * cache information and invalidate them based on many variables.
 *
 * Each item is identified by two code:
 *	- cch_id: Global identifier, for example INDEX, SERVICE, SEARCH, etc..
 *	- cch_code: Specific identifier, for example page URI, script name, search
 *	  conditions, etc..
 */
class WCPageCache {
	/**
	 * @var WCPageCache Pointer to the singleton instance.
	 */
	protected static $_Instance;
	/**
	 * @var string[string] This variable hold a list of cached items text
	 * associated with their ids.
	 */
	protected $_strings = array();
	/**
	 * @var WCConnection Database pointer shortcut.
	 */
	protected $_db;
	/**
	 * @var string Database tables' prefix shortcut.
	 */
	protected $_dbprefix;
	/**
	 * @var string This variable holds the specific identifier for current
	 * cache item.
	 */
	protected $_code;
	/**
	 * @var boolean This flag indicates if cache system is on or not.
	 */
	protected $_enabled;
	/**
	 * @var string This flag indicates if current cache items depend on any
	 * group's change (values 'Y' and 'N')
	 */
	protected $_anyGroup = "N";
	/**
	 * @var string This flag indicates if current cache items depend on any
	 * collection's change (values 'Y' and 'N')
	 */
	protected $_anyCollection = "N";
	/**
	 * @var string This flag indicates if current cache items depend on any
	 * tag's change (values 'Y' and 'N')
	 */
	protected $_anyTag = "N";
	/**
	 * @var string This flag indicates if current cache items depend on any
	 * directory's change (values 'Y' and 'N')
	 */
	protected $_anyDirectory = "N";
	/**
	 * @var string This flag indicates if current cache items depend on any
	 * comic's change (values 'Y' and 'N')
	 */
	protected $_anyComic = "N";
	/**
	 * @var string This flag indicates if current cache items depend on any
	 * rule's change (values 'Y' and 'N')
	 */
	protected $_anyRule = "N";
	/**
	 * @var boolean This flag indicates if current access is being made from
	 * a shell or from web.
	 */
	protected $_onShell = false;
	/**
	 * @var mixed[string] This structure holds the statistical information of
	 * last requested cache item.
	 */
	protected $_lastInfo = array(
		"id" => false,
		"code" => false,
		"compressed" => false,
		"uncompressed" => false,
		"rate" => false
	);
	/**
	 * Class constructor.
	 */
	protected function __construct() {
		//
		// Loading global pointers.
		global $wcDatabaseAccess;
		global $wcDefaults;
		global $wcUser;
		//
		// Loading shortcuts.
		// @{
		$this->_db = WCConnection::Instance();
		$this->_dbprefix = $wcDatabaseAccess['prefix'];
		// @}
		//
		// Building current cache code.
		if(!defined("__SHELL__")) {
			//
			// On web access, code is based on URI.
			$this->_code = "{$_SERVER["SCRIPT_NAME"]}?{$_SERVER["QUERY_STRING"]}";
		} else {
			//
			// On shell access, code is based on the script name.
			$this->_code = $_SERVER["SCRIPT_NAME"];
			$this->_onShell = true;
		}
		//
		// If there's a user specified, its cache items may differ from
		// other users.
		if($wcUser) {
			$this->_code = "{$wcUser->id()}|{$this->_code}";
		}
		//
		// Encoding code to avoid dangerous characters.
		$this->_code = base64_encode($this->_code);
		//
		// Checking if cache is disabled or not.
		$this->_enabled = $wcDefaults['page-cache'] && !isset($_REQUEST['NOPAGECAGE']);
	}
	/**
	 * Prevent users to clone the instance.
	 */
	public function __clone() {
		trigger_error(__CLASS__.": Clone is not allowed.", E_USER_ERROR);
	}
	//
	// Public methods.
	/**
	 * This method allows add more text to a caching item.
	 *
	 * @param string $id Cached item's id.
	 * @param string $string Text to be appended.
	 */
	public function append($id, $string) {
		//
		// Checking if text can be appended or assigned.
		if(isset($this->_strings[$id])) {
			$this->_strings[$id] .= $string;
		} else {
			$this->_strings[$id] = $string;
		}
	}
	/**
	 * Sets the dependent on any collection flag.
	 *
	 * @param boolean $status Status to be set, by default true.
	 */
	public function anyCollection($status = true) {
		$this->_anyCollection = $status ? "Y" : "N";
	}
	/**
	 * Sets the dependent on any comic flag.
	 *
	 * @param boolean $status Status to be set, by default true.
	 */
	public function anyComic($status = true) {
		$this->_anyComic = $status ? "Y" : "N";
	}
	/**
	 * Sets the dependent on any directory flag.
	 *
	 * @param boolean $status Status to be set, by default true.
	 */
	public function anyDirectory($status = true) {
		$this->_anyDirectory = $status ? "Y" : "N";
	}
	/**
	 * Sets the dependent on any group flag.
	 *
	 * @param boolean $status Status to be set, by default true.
	 */
	public function anyGroup($status = true) {
		$this->_anyGroup = $status ? "Y" : "N";
	}
	/**
	 * Sets the dependent on any rule flag.
	 *
	 * @param boolean $status Status to be set, by default true.
	 */
	public function anyRule($status = true) {
		$this->_anyRule = $status ? "Y" : "N";
	}
	/**
	 * This method allows to obtain a list of flags indicating dependencies
	 * for current cached items.
	 *
	 * @return boolean[string] Returns a list of flags.
	 */
	public function anyStatus() {
		return array(
			"group" => $this->_anyGroup == "Y",
			"collection" => $this->_anyCollection == "Y",
			"tag" => $this->_anyTag == "Y",
			"directory" => $this->_anyDirectory == "Y",
			"comic" => $this->_anyComic == "Y",
			"rule" => $this->_anyRule == "Y"
		);
	}
	/**
	 * Sets the dependent on any tag flag.
	 *
	 * @param boolean $status Status to be set, by default true.
	 */
	public function anyTag($status = true) {
		$this->_anyTag = $status ? "Y" : "N";
	}
	/**
	 * This method allows to access current cached item code.
	 *
	 * @return string Returns current code.
	 */
	public function code() {
		return $this->_code;
	}
	/**
	 * This method allows to disable cache system.
	 *
	 * @return boolean Returns final status.
	 */
	public function disabled() {
		$this->_enabled = false;
		return $this->enabled();
	}
	/**
	 * This method allows to know if cache system is enabled.
	 *
	 * @return boolean Returns cache system status.
	 */
	public function enabled() {
		return $this->_enabled;
	}
	/**
	 * This method allows to get a cached item on database and returns it.
	 *
	 * @param string $id Item's id to look for.
	 * @param boolean $stats Enables/Disables statistic information prompting.
	 * @param boolean $code This optional parameter allows to avoid internal
	 * global code for cached items. Also, it forces cache system to work even
	 * if it's disabled.
	 * @return string Returns cached item's text. On failure it returns false.
	 */
	public function get($id, $stats = true, $code = false) {
		//
		// Getting the global code to be used.
		$auxCode = $code === false ? $this->code() : $code;
		//
		// Checking that cache system is enable and that this item is not
		// yet loaded.
		if(($this->enabled() || $code) && !isset($this->_strings[$id])) {
			//
			// Cleaning obsolete information of this item.
			$this->clear($id, $code);
			//
			// Cleaning last statistics information.
			foreach($this->_lastInfo as $key => $value) {
				$this->_lastInfo[$key] = false;
			}
			//
			// Creating a statement to request cached item's data.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__;
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "select  cch_data,\n";
				$query .= "        cch_length,\n";
				$query .= "        cch_full_length\n";
				$query .= "from    {$this->_dbprefix}cache\n";
				$query .= "where   cch_id   = :id\n";
				$query .= " and    cch_code = :code";
				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Building database parameters.
			$params = array(
				":id" => $id,
				":code" => $auxCode
			);
			//
			// Requesting item's data.
			if($stmt->execute($params) && $stmt->rowCount() > 0) {
				//
				// Fetching information.
				$row = $stmt->fetch();
				//
				// Uncompressing information from database. This
				// is made to reduce database storage space.
				$this->_strings[$id] = gzuncompress(base64_decode($row["cch_data"]));
				//
				// Building statistic information.
				// @{
				$this->_lastInfo["id"] = $id;
				$this->_lastInfo["code"] = $auxCode;
				$this->_lastInfo["compressed"] = $row["cch_length"];
				$this->_lastInfo["uncompressed"] = $row["cch_full_length"];
				$this->_lastInfo["rate"] = round($row["cch_length"] / ($row["cch_full_length"] > 0 ? $row["cch_full_length"] : 1) * 100);
				// @}
				//
				// Prompting summary.
				if($stats) {
					$this->_strings[$id] .= "<!--\n";
					$this->_strings[$id] .= "CACHE\n";
					$this->_strings[$id] .= "ID:   {$this->_lastInfo["id"]}\n";
					$this->_strings[$id] .= "CODE: {$this->_lastInfo["code"]}\n\n";
					$this->_strings[$id] .= "COMPRESSED:   {$this->_lastInfo["compressed"]}\n";
					$this->_strings[$id] .= "UNCOMPRESSED: {$this->_lastInfo["uncompressed"]}\n";
					$this->_strings[$id] .= "RATE:         {$this->_lastInfo["rate"]}%\n";
					$this->_strings[$id] .= "-->\n";
				}
			}
		}
		//
		// Returning cached results.
		return isset($this->_strings[$id]) ? $this->_strings[$id] : false;
	}
	/**
	 * This method allows to invalidate cached items related to certain comic.
	 *
	 * @param WCComic $comic Comic to use as invalidating relation.
	 */
	public function invalidateByComic(WCComic &$comic) {
		//
		// Checking if there's a comic specified.
		if($comic) {
			//
			// Creating a statement to invalidate cached items.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__;
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "update  {$this->_dbprefix}cache\n";
				$query .= "set     cch_invalid = 'Y'\n";
				$query .= "where   cch_comic     = :id";
				$query .= " or     cch_any_comic = 'Y'";
				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Building database parameters.
			$params = array(":id" => $comic->id());
			//
			// Invalidation items.
			$stmt->execute($params);
			//
			// Loading global pointers.
			global $wcTagsHolder;
			//
			// Invalidating tags where the comic appears.
			foreach($wcTagsHolder->tagsOf($comic) as $tag) {
				/** @todo this is too slow therefore it is commented. */
				//$this->invalidateByTag($tag);
			}
			//
			// Invalidating the directory where the comic is stored.
			$auxdir = $comic->directory();
			$this->invalidateByDirectory($auxdir);
		}
	}
	/**
	 * This method allows to invalidate cached items related to certain
	 * directory.
	 *
	 * @param WCDirectory $directory Directory to use as invalidating
	 * relation.
	 */
	public function invalidateByDirectory(WCDirectory &$directory) {
		//
		// Checking if there's a directory specified.
		if($directory) {
			//
			// Creating a statement to invalidate cached items.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__;
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "update  {$this->_dbprefix}cache\n";
				$query .= "set     cch_invalid = 'Y'\n";
				$query .= "where   cch_directory     = :id";
				$query .= " or     cch_any_directory = 'Y'";
				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Building database parameters.
			$params = array(":id" => $directory->id());
			//
			// Invalidation items.
			$stmt->execute($params);
		}
	}
	/**
	 * This method allows to invalidate cached items related to certain rule.
	 *
	 * @param WCRule $rule Rule to use as invalidating relation.
	 */
	public function invalidateByRule(WCRule &$rule) {
		//
		// Checking if there's a rule specified.
		if($rule) {
			//
			// Creating a statement to invalidate cached items.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__;
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "update  {$this->_dbprefix}cache\n";
				$query .= "set     cch_invalid = 'Y'\n";
				$query .= "where   cch_rule     = :id";
				$query .= " or     cch_any_rule = 'Y'";
				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Building database parameters.
			$params = array(":id" => $rule->id);
			//
			// Invalidation items.
			$stmt->execute($params);
		}
	}
	/**
	 * This method allows to invalidate cached items related to certain tag.
	 *
	 * @param WCTag $tag Tag to use as invalidating relation.
	 */
	public function invalidateByTag(WCTag &$tag) {
		//
		// Checking if there's a tag specified.
		if($tag) {
			//
			// Creating a statement to invalidate cached items.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__;
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "update  {$this->_dbprefix}cache\n";
				$query .= "set     cch_invalid = 'Y'\n";
				$query .= "where   cch_tag     = :id";
				$query .= " or     cch_any_tag = 'Y'";
				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Building database parameters.
			$params = array(":id" => $tag->id);
			//
			// Invalidation items.
			$stmt->execute($params);
		}
	}
	/**
	 * Allows to get a structure with information for the last cached item
	 * loaded.
	 *
	 * @return mixed[string] Returns the last cached item information.
	 */
	public function lastInfo() {
		return $this->_lastInfo;
	}
	/**
	 * This method deletes a specific cached item based on its ID
	 *
	 * @param string $id Global identifier.
	 * @param string $code Specific code. When absent internal code is used.
	 */
	public function reset($id, $code = false) {
		//
		// Getting the global code to be used.
		$auxCode = $code === false ? $this->code() : $code;
		//
		// Creating a statement to remove a cached item.
		// @{
		$stmt = null;
		$stmtId = __CLASS__."::".__FUNCTION__;
		if($this->_db->has($stmtId)) {
			$stmt = $this->_db->get($stmtId);
		} else {
			$query = "delete from     {$this->_dbprefix}cache\n";
			$query .= "where           cch_id   = :id\n";
			$query .= " and            cch_code = :code\n";
			$stmt = $this->_db->prepare($stmtId, $query);
		}
		// @}
		//
		// Building database parameters.
		$params = array(
			":id" => $id,
			":code" => $auxCode
		);
		//
		// Removing item.
		$stmt->execute($params);
		//
		// Removing memory cache of the item.
		unset($this->_strings[$id]);
	}
	/**
	 * This method deletes every cached item based on their global ID.
	 *
	 * @param string $id Global identifier.
	 */
	public function resetFullId($id) {
		//
		// Creating a statement to remove a cached items.
		// @{
		$stmt = null;
		$stmtId = __CLASS__."::".__FUNCTION__;
		if($this->_db->has($stmtId)) {
			$stmt = $this->_db->get($stmtId);
		} else {
			$query = "delete from     {$this->_dbprefix}cache\n";
			$query .= "where           cch_id   = :id\n";
			$stmt = $this->_db->prepare($stmtId, $query);
		}
		// @}
		//
		// Building database parameters.
		$params = array(":id" => $id);
		//
		// Removing items.
		$stmt->execute($params);
		//
		// Removing memory cache of the item.
		unset($this->_strings[$id]);
	}
	/**
	 * This method pushes an item into database to keep it cached.
	 *
	 * @param string $id Global identifier.
	 * @param string $code Specific code. When absent internal code is used.
	 * When present, forces cache system to save even if it's disabled.
	 */
	public function save($id, $code = false) {
		//
		// Getting the global code to be used.
		$auxCode = $code === false ? $this->code() : $code;
		//
		// Checking if there's text to store and if cache system is
		// enabled.
		if(($this->enabled() || $code) && isset($this->_strings[$id])) {
			//
			// Destroying previous cached data in database (memory
			// cache is useful).
			// @{
			$data = $this->_strings[$id];
			$this->reset($id, $code);
			$this->_strings[$id] = $data;
			// @}
			//
			// Compressing text to reduce database storage use.
			$data = base64_encode(gzcompress($this->_strings[$id], WC_CACHE_COMPRESS_LEVEL));
			//
			// Calculation how many bytes have to be stored.
			$gzlen = strlen($data);
			//
			// Calculation how many bytes have the original text.
			$len = strlen($this->_strings[$id]);
			//
			// Checking if MySQL can handle this amount of
			// information.
			if($gzlen < 65535) {
				//
				// Loading global pointers.
				global $wcGroup;
				global $wcCollection;
				global $wcTag;
				global $wcDirectory;
				global $wcComic;
				global $wcRule;
				//
				// Creating a statement to insert a new cached
				// item.
				// @{
				$stmt = null;
				$stmtId = __CLASS__."::".__FUNCTION__;
				if($this->_db->has($stmtId)) {
					$stmt = $this->_db->get($stmtId);
				} else {
					$query = "insert\n";
					$query .= "        into {$this->_dbprefix}cache (cch_id, cch_code, cch_data, cch_length, cch_full_length,\n";
					$query .= "                cch_group, cch_collection, cch_tag, cch_directory, cch_comic, cch_rule,\n";
					$query .= "                cch_any_group, cch_any_collection, cch_any_tag, cch_any_directory, cch_any_comic, cch_any_rule)\n";
					$query .= "        values (:id, :code, :data, :gzlen, :len, :group, :collection, :tag, :directory, :comic, :rule,\n";
					$query .= "                :any_group, :any_collection, :any_tag, :any_directory, :any_comic, :any_rule)\n";
					$stmt = $this->_db->prepare($stmtId, $query);
				}
				// @}
				//
				// Building database parameters.
				$params = array(
					":id" => $id,
					":code" => $auxCode,
					":data" => $data,
					":gzlen" => $gzlen,
					":len" => $len,

					":group" => $wcGroup ? $wcGroup->id : null,
					":collection" => $wcCollection ? $wcCollection->id : null,
					":tag" => $wcTag ? $wcTag->id : null,
					":directory" => $wcDirectory ? $wcDirectory->id : null,
					":comic" => $wcComic ? $wcComic->id : null,
					":rule" => $wcRule ? $wcRule->id : null,

					":any_group" => $this->_anyGroup,
					":any_collection" => $this->_anyCollection,
					":any_tag" => $this->_anyTag,
					":any_directory" => $this->_anyDirectory,
					":any_comic" => $this->_anyComic,
					":any_rule" => $this->_anyRule
				);
				//
				// Adding new cached item.
				$stmt->execute($params);
			} else {
				//
				// At this point, information to be cached can't
				// be handled by MySQL.
				echo "<!--\ncache larger than 65535 bytes\n-->\n";
			}
		}
	}
	//
	// Protected methods.
	/**
	 * This method deletes a specific cached item based on its ID when it's
	 * old or invalid.
	 *
	 * @param string $id Global identifier.
	 * @param string $code Specific code. When absent internal code is used.
	 */
	protected function clear($id, $code = false) {
		//
		// Getting the global code to be used.
		$auxCode = $code === false ? $this->code() : $code;
		//
		// Creating a statement to remove a cached item.
		// @{
		$stmt = null;
		$stmtId = __CLASS__."::".__FUNCTION__;
		if($this->_db->has($stmtId)) {
			$stmt = $this->_db->get($stmtId);
		} else {
			$query = "delete from {$this->_dbprefix}cache\n";
			$query .= "where       cch_id   = :id\n";
			$query .= " and        cch_code = :code\n";
			$query .= " and        (cch_invalid = 'Y' or (now() - cch_date) > 3600)\n";
			$stmt = $this->_db->prepare($stmtId, $query);
		}
		// @}
		//
		// Building database parameters.
		$params = array(
			":id" => $id,
			":code" => $auxCode
		);
		//
		// Removing item.
		$stmt->execute($params);
		//
		// Removing memory cache of the item.
		if($stmt->rowCount() > 0 && isset($this->_strings[$id])) {
			unset($this->_strings[$id]);
		}
	}
	//
	// Public class methods.
	/**
	 * This class method allows to get the sole instance of this engine.
	 *
	 * @return WCPageCache Returns the singleton instance of this class.
	 */
	public static function Instance() {
		//
		// Checking if there's no instance yet.
		if(!isset(self::$_Instance)) {
			$c = __CLASS__;
			//
			// Creating the sole instance.
			self::$_Instance = new $c();
		}
		//
		// Returning the class instance.
		return self::$_Instance;
	}
}
?>
